2 // ShareTableViewDataSource.swift
5 // Created by Claudio Cambra on 27/2/24.
11 import NextcloudFileProviderKit
12 import NextcloudCapabilitiesKit
15 class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate {
16 private let shareItemViewIdentifier = NSUserInterfaceItemIdentifier("ShareTableItemView")
17 private let shareItemViewNib = NSNib(nibNamed: "ShareTableItemView", bundle: nil)
18 private let reattemptInterval: TimeInterval = 3.0
20 let kit = NextcloudKit.shared
22 var uiDelegate: ShareViewDataSourceUIDelegate?
23 var sharesTableView: NSTableView? {
25 sharesTableView?.register(shareItemViewNib, forIdentifier: shareItemViewIdentifier)
26 sharesTableView?.rowHeight = 42.0 // Height of view in ShareTableItemView XIB
27 sharesTableView?.dataSource = self
28 sharesTableView?.delegate = self
29 sharesTableView?.reloadData()
32 var capabilities: Capabilities?
34 private(set) var itemURL: URL?
35 private(set) var itemServerRelativePath: String?
36 private(set) var shares: [NKShare] = [] {
37 didSet { Task { @MainActor in sharesTableView?.reloadData() } }
39 private(set) var account: Account? {
41 guard let account = account else { return }
43 account: account.ncKitAccount,
44 urlBase: account.serverUrl,
45 user: account.username,
46 userId: account.username,
47 password: account.password,
48 userAgent: "Nextcloud-macOS/FileProviderUIExt",
55 func loadItem(url: URL) {
56 itemServerRelativePath = nil
64 DispatchQueue.main.async {
65 Timer.scheduledTimer(withTimeInterval: self.reattemptInterval, repeats: false) { _ in
66 Task { await self.reload() }
72 guard let itemURL else {
73 presentError("No item URL, cannot reload data!")
76 guard let itemIdentifier = await withCheckedContinuation({
77 (continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
78 NSFileProviderManager.getIdentifierForUserVisibleFile(
80 ) { identifier, domainIdentifier, error in
81 defer { continuation.resume(returning: identifier) }
82 guard error == nil else {
83 self.presentError("No item with identifier: \(error.debugDescription)")
88 presentError("Could not get identifier for item, no shares can be acquired.")
93 let connection = try await serviceConnection(url: itemURL, interruptionHandler: {
94 Logger.sharesDataSource.error("Service connection interrupted")
96 guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
97 let credentials = await connection.credentials() as? Dictionary<String, String>,
98 let convertedAccount = Account(dictionary: credentials),
99 !convertedAccount.password.isEmpty
101 presentError("Failed to get details from File Provider Extension. Retrying.")
105 let serverPathString = serverPath as String
106 itemServerRelativePath = serverPathString
107 account = convertedAccount
108 await sharesTableView?.deselectAll(self)
109 capabilities = await fetchCapabilities()
110 guard capabilities != nil else { return }
111 guard capabilities?.filesSharing?.apiEnabled == true else {
112 presentError("Server does not support shares.")
115 guard let account else {
116 presentError("Account data is unavailable, cannot reload data!")
119 guard let itemMetadata = await fetchItemMetadata(
120 itemRelativePath: serverPathString, account: account, kit: kit
122 presentError("Unable to retrieve file metadata...")
125 guard itemMetadata.permissions.contains("R") == true else {
126 presentError("This file cannot be shared.")
129 shares = await fetch(
130 itemIdentifier: itemIdentifier, itemRelativePath: serverPathString
133 presentError("Could not reload data: \(error), will try again.")
139 itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String
140 ) async -> [NKShare] {
141 Task { @MainActor in uiDelegate?.fetchStarted() }
142 defer { Task { @MainActor in uiDelegate?.fetchFinished() } }
144 let rawIdentifier = itemIdentifier.rawValue
145 Logger.sharesDataSource.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
147 guard let account else {
148 self.presentError("NextcloudKit instance or account is unavailable, cannot fetch shares!")
152 let parameter = NKShareParameter(path: itemRelativePath)
154 return await withCheckedContinuation { continuation in
156 parameters: parameter, account: account.ncKitAccount
157 ) { account, shares, data, error in
158 let shareCount = shares?.count ?? 0
159 Logger.sharesDataSource.info("Received \(shareCount, privacy: .public) shares")
160 defer { continuation.resume(returning: shares ?? []) }
161 guard error == .success else {
162 self.presentError("Error fetching shares: \(error.errorDescription)")
169 private static func generateInternalShare(for file: NKFile) -> NKShare {
170 let internalShare = NKShare()
171 internalShare.shareType = NKShare.ShareType.internalLink.rawValue
172 internalShare.url = file.urlBase + "/index.php/f/" + file.fileId
173 internalShare.account = file.account
174 internalShare.displaynameOwner = file.ownerDisplayName
175 internalShare.displaynameFileOwner = file.ownerDisplayName
176 internalShare.path = file.path
180 private func fetchCapabilities() async -> Capabilities? {
181 guard let account else {
182 self.presentError("Could not fetch capabilities as account is invalid.")
186 return await withCheckedContinuation { continuation in
187 kit.getCapabilities(account: account.ncKitAccount) { account, data, error in
188 guard error == .success, let capabilitiesJson = data?.data else {
189 self.presentError("Error getting server caps: \(error.errorDescription)")
190 continuation.resume(returning: nil)
193 Logger.sharesDataSource.info("Successfully retrieved server share capabilities")
194 continuation.resume(returning: Capabilities(data: capabilitiesJson))
199 private func presentError(_ errorString: String) {
200 Logger.sharesDataSource.error("\(errorString, privacy: .public)")
201 Task { @MainActor in self.uiDelegate?.showError(errorString) }
204 // MARK: - NSTableViewDataSource protocol methods
206 @objc func numberOfRows(in tableView: NSTableView) -> Int {
210 // MARK: - NSTableViewDelegate protocol methods
212 @objc func tableView(
213 _ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int
215 let share = shares[row]
216 guard let view = tableView.makeView(
217 withIdentifier: shareItemViewIdentifier, owner: self
218 ) as? ShareTableItemView else {
219 Logger.sharesDataSource.error("Acquired item view from table is not a share item view!")
226 @objc func tableViewSelectionDidChange(_ notification: Notification) {
227 guard let selectedRow = sharesTableView?.selectedRow, selectedRow >= 0 else {
228 Task { @MainActor in uiDelegate?.hideOptions(self) }
231 let share = shares[selectedRow]
232 Task { @MainActor in uiDelegate?.showOptions(share: share) }